/*
 * X509CertUtil.java
 * This file is part of Portecle, a multipurpose keystore and certificate tool.
 *
 * Copyright © 2004 Wayne Grant, waynedgrant@hotmail.com
 *             2004-2007 Ville Skyttä, ville.skytta@iki.fi
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301, USA.
 */

package com.googlecode.bluetools.common.security;

import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.StringWriter;
import java.math.BigInteger;
import java.security.GeneralSecurityException;
import java.security.InvalidKeyException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.Principal;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.SignatureException;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509CRL;
import java.security.cert.X509Certificate;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.List;
import java.util.ResourceBundle;
import java.util.Vector;

import javax.security.auth.x500.X500Principal;

import org.bouncycastle.asn1.x509.X509Name;
import org.bouncycastle.jce.PKCS10CertificationRequest;
import org.bouncycastle.jce.X509Principal;
import org.bouncycastle.openssl.PEMReader;
import org.bouncycastle.openssl.PEMWriter;
import org.bouncycastle.x509.X509V3CertificateGenerator;

import com.googlecode.bluetools.common.utils.StringUtil;

/**
 * Provides utility methods relating to X509 Certificates, CRLs and CSRs.
 */
public final class X509CertUtil extends SecurityUtil {

	/** PKCS #7 encoding name */
	public static final String PKCS7_ENCODING = "PKCS7";

	/** PkiPath encoding name */
	public static final String PKIPATH_ENCODING = "PkiPath";

	/** OpenSSL PEM encoding name */
	public static final String OPENSSL_PEM_ENCODING = "OpenSSL_PEM";

	/** Resource bundle */
	private static ResourceBundle m_res = ResourceBundle.getBundle("com/googlecode/bluetools/common/security/resources");

	/** Type name for X.509 certificates */
	private static final String X509_CERT_TYPE = "X.509";

	/**
	 * Private to prevent construction.
	 */
	private X509CertUtil() {
	}

	/**
	 * Load one or more certificates from the specified file.
	 * 
	 * @param fCertFile The file to load certificates from
	 * @param encoding The certification path encoding. If null, treat as a normal certificate, not certification path.
	 *            Use one of the <code>*_ENCODING</code> constants here.
	 * @return The certificates
	 * @throws CryptoException Problem encountered while loading the certificate(s)
	 */
	public static X509Certificate[] loadCertificates(File fCertFile, String encoding) throws CryptoException {
		try {
			FileInputStream fis = new FileInputStream(fCertFile);
			return loadCertificates(fis, encoding);
		}
		catch (Exception ex) {
			throw new CryptoException(m_res.getString("NoLoadCertificate.exception.message"), ex);
		}
	}

	/**
	 * Load one or more certificates from the specified file.
	 * 
	 * @param in The inputstream to load certificates from
	 * @param encoding The certification path encoding. If null, treat as a normal certificate, not certification path.
	 *            Use one of the <code>*_ENCODING</code> constants here.
	 * @return The certificates
	 * @throws CryptoException Problem encountered while loading the certificate(s)
	 */
	public static X509Certificate[] loadCertificates(InputStream in, String encoding) throws CryptoException {
		ArrayList vCerts = new ArrayList();
		try {
			CertificateFactory cf = CertificateFactory.getInstance(X509_CERT_TYPE);
			Collection coll = null;

			if (OPENSSL_PEM_ENCODING.equals(encoding)) {
				// Special case; this is not a real JCE supported encoding.
				PEMReader pr = new PEMReader(new InputStreamReader(in), null, cf.getProvider().getName());
				/*
				 * These beasts can contain just about anything, and unfortunately the PEMReader API (as of BC 1.25 to
				 * 1.31) won't allow us to really skip things we're not interested in; stuff happens already in
				 * readObject(). This may cause some weird exception messages for non-certificate objects in the
				 * "stream", for example passphrase related ones for protected private keys. Well, I guess this is
				 * better than nothing anyway... :(
				 */
				Object cert;
				while ((cert = pr.readObject()) != null) {
					if (cert instanceof X509Certificate) {
						// "Short-circuit" into vCerts, not using coll.
						vCerts.add(cert);
					}
					// Skip other stuff, at least for now.
				}
			}
			else if (encoding != null) {
				// Try it as a certification path of the specified type
				coll = cf.generateCertPath(in, encoding).getCertificates();
			}
			else {
				// "Normal" certificate(s)
				coll = cf.generateCertificates(in);
			}

			if (coll != null) {
				for (Iterator iter = coll.iterator(); iter.hasNext();) {
					X509Certificate cert = (X509Certificate) iter.next();
					if (cert != null) {
						vCerts.add(cert);
					}
				}
			}
		}
		// Some RuntimeExceptions which really ought to be
		// CertificateExceptions may be thrown from cf.generateCert* above,
		// for example Sun's PKCS #7 parser tends to throw them... :P
		catch (Exception ex) {
			// TODO: don't throw if vCerts non-empty (eg. OpenSSL PEM above)?
			throw new CryptoException(m_res.getString("NoLoadCertificate.exception.message"), ex);
		}
		finally {
			if (in != null) {
				try {
					in.close();
				}
				catch (IOException ex) {
					// Ignore
				}
			}
		}

		return (X509Certificate[]) vCerts.toArray(new X509Certificate[vCerts.size()]);
	}

	/**
	 * Load a CRL from the specified file.
	 * 
	 * @param fCRLFile The file to load CRL from
	 * @return The CRL
	 * @throws CryptoException Problem encountered while loading the CRL
	 * @throws java.io.FileNotFoundException If the CRL file does not exist, is a directory rather than a regular file,
	 *             or for some other reason cannot be opened for reading
	 * @throws IOException An I/O error occurred
	 */
	public static X509CRL loadCRL(File fCRLFile) throws CryptoException, IOException {
		FileInputStream fis = new FileInputStream(fCRLFile);
		try {
			CertificateFactory cf = CertificateFactory.getInstance(X509_CERT_TYPE);
			X509CRL crl = (X509CRL) cf.generateCRL(fis);
			return crl;
		}
		catch (GeneralSecurityException ex) {
			throw new CryptoException(m_res.getString("NoLoadCrl.exception.message"), ex);
		}
		finally {
			try {
				fis.close();
			}
			catch (IOException ex) {
				// Ignore
			}
		}
	}

	/**
	 * Load a CSR from the specified file.
	 * 
	 * @param fCSRFile The file to load CSR from
	 * @return The CSR
	 * @throws CryptoException Problem encountered while loading the CSR
	 * @throws java.io.FileNotFoundException If the CSR file does not exist, is a directory rather than a regular file,
	 *             or for some other reason cannot be opened for reading
	 * @throws IOException An I/O error occurred
	 */
	public static PKCS10CertificationRequest loadCSR(File fCSRFile) throws CryptoException, IOException {
		// TODO: handle DER encoded requests too?
		PEMReader in = new PEMReader(new InputStreamReader(new FileInputStream(fCSRFile)));
		try {
			PKCS10CertificationRequest csr = (PKCS10CertificationRequest) in.readObject();
			if (!csr.verify()) {
				throw new CryptoException(m_res.getString("NoVerifyCsr.exception.message"));
			}
			return csr;
		}
		catch (ClassCastException ex) {
			throw new CryptoException(m_res.getString("NoLoadCsr.exception.message"), ex);
		}
		catch (GeneralSecurityException ex) {
			throw new CryptoException(m_res.getString("NoLoadCsr.exception.message"), ex);
		}
		finally {
			try {
				in.close();
			}
			catch (IOException ex) {
				// Ignore
			}
		}
	}

	/**
	 * Convert the supplied array of certificate objects into X509Certificate objects.
	 * 
	 * @param certsIn The Certificate objects
	 * @return The converted X509Certificate objects
	 * @throws CryptoException A problem occurred during the conversion
	 */
	public static X509Certificate[] convertCertificates(Certificate[] certsIn) throws CryptoException {
		X509Certificate[] certsOut = new X509Certificate[certsIn.length];

		for (int iCnt = 0; iCnt < certsIn.length; iCnt++) {
			certsOut[iCnt] = convertCertificate(certsIn[iCnt]);
		}

		return certsOut;
	}

	/**
	 * Convert the supplied certificate object into an X509Certificate object.
	 * 
	 * @param certIn The Certificate object
	 * @return The converted X509Certificate object
	 * @throws CryptoException A problem occurred during the conversion
	 */
	public static X509Certificate convertCertificate(Certificate certIn) throws CryptoException {
		try {
			// We could request BC here in order to gain support for certs
			// with > 2048 bit RSA keys also on Java 1.4. But unless there's
			// a way to eg. read JKS keystores containing such certificates
			// on Java 1.4 (think eg. importing such CA certs), that would
			// just help the user shoot herself in the foot...
			CertificateFactory cf = CertificateFactory.getInstance(X509_CERT_TYPE);
			ByteArrayInputStream bais = new ByteArrayInputStream(certIn.getEncoded());
			return (X509Certificate) cf.generateCertificate(bais);
		}
		catch (CertificateException ex) {
			throw new CryptoException(m_res.getString("NoConvertCertificate.exception.message"), ex);
		}
	}

	/**
	 * Attempt to order the supplied array of X.509 certificates in issued to to issued from order.
	 * 
	 * @param certs The X.509 certificates in order
	 * @return The ordered X.509 certificates
	 * @deprecated use {@link #sortCertificates(List)} instead
	 */
	public static X509Certificate[] orderX509CertChain(X509Certificate[] certs) {
		int iOrdered = 0;
		X509Certificate[] tmpCerts = (X509Certificate[]) certs.clone();
		X509Certificate[] orderedCerts = new X509Certificate[certs.length];

		X509Certificate issuerCert = null;

		// Find the root issuer (ie certificate where issuer is the same
		// as subject)
		for (int iCnt = 0; iCnt < tmpCerts.length; iCnt++) {
			X509Certificate aCert = tmpCerts[iCnt];
			if (aCert.getIssuerDN().equals(aCert.getSubjectDN())) {
				issuerCert = aCert;
				orderedCerts[iOrdered] = issuerCert;
				iOrdered++;
			}
		}

		// Couldn't find a root issuer so just return the un-ordered array
		if (issuerCert == null) {
			return certs;
		}

		// Keep making passes through the array of certificates looking for the
		// next certificate in the chain until the links run out
		while (true) {
			boolean bFoundNext = false;
			for (int iCnt = 0; iCnt < tmpCerts.length; iCnt++) {
				X509Certificate aCert = tmpCerts[iCnt];

				// Is this certificate the next in the chain?
				if (aCert.getIssuerDN().equals(issuerCert.getSubjectDN()) && aCert != issuerCert) {
					// Yes
					issuerCert = aCert;
					orderedCerts[iOrdered] = issuerCert;
					iOrdered++;
					bFoundNext = true;
					break;
				}
			}
			if (!bFoundNext) {
				break;
			}
		}

		// Resize array
		tmpCerts = new X509Certificate[iOrdered];
		System.arraycopy(orderedCerts, 0, tmpCerts, 0, iOrdered);

		// Reverse the order of the array
		orderedCerts = new X509Certificate[iOrdered];

		for (int iCnt = 0; iCnt < iOrdered; iCnt++) {
			orderedCerts[iCnt] = tmpCerts[tmpCerts.length - 1 - iCnt];
		}

		return orderedCerts;
	}

	/**
	 * DER encode a certificate.
	 * 
	 * @return The binary encoding
	 * @param cert The certificate
	 * @throws CryptoException If there was a problem encoding the certificate
	 */
	public static byte[] getCertEncodedDer(X509Certificate cert) throws CryptoException {
		try {
			return cert.getEncoded();
		}
		catch (CertificateException ex) {
			throw new CryptoException(m_res.getString("NoDerEncode.exception.message"), ex);
		}
	}

	/**
	 * PKCS #7 encode a certificate.
	 * 
	 * @return The PKCS #7 encoded certificate
	 * @param cert The certificate
	 * @throws CryptoException If there was a problem encoding the certificate
	 */
	public static byte[] getCertEncodedPkcs7(X509Certificate cert) throws CryptoException {
		return getCertsEncodedPkcs7(new X509Certificate[] {
			cert
		});
	}

	/**
	 * PKCS #7 encode a number of certificates.
	 * 
	 * @return The PKCS #7 encoded certificates
	 * @param certs The certificates
	 * @throws CryptoException If there was a problem encoding the certificates
	 */
	public static byte[] getCertsEncodedPkcs7(X509Certificate[] certs) throws CryptoException {
		return getCertsEncoded(certs, PKCS7_ENCODING, "NoPkcs7Encode.exception.message");
	}

	/**
	 * PkiPath encode a certificate.
	 * 
	 * @return The PkiPath encoded certificate
	 * @param cert The certificate
	 * @throws CryptoException If there was a problem encoding the certificate
	 */
	public static byte[] getCertEncodedPkiPath(X509Certificate cert) throws CryptoException {
		return getCertsEncodedPkiPath(new X509Certificate[] {
			cert
		});
	}

	/**
	 * PkiPath encode a number of certificates.
	 * 
	 * @return The PkiPath encoded certificates
	 * @param certs The certificates
	 * @throws CryptoException If there was a problem encoding the certificates
	 */
	public static byte[] getCertsEncodedPkiPath(X509Certificate[] certs) throws CryptoException {
		return getCertsEncoded(certs, PKIPATH_ENCODING, "NoPkiPathEncode.exception.message");
	}

	/**
	 * Encode a number of certificates using the given encoding.
	 * 
	 * @return The encoded certificates
	 * @param certs The certificates
	 * @param encoding The encoding
	 * @param errkey The error message key to use in the possibly occurred exception
	 * @throws CryptoException If there was a problem encoding the certificates
	 */
	private static byte[] getCertsEncoded(X509Certificate[] certs, String encoding, String errkey)
			throws CryptoException {
		try {
			CertificateFactory cf = CertificateFactory.getInstance(X509_CERT_TYPE);
			return cf.generateCertPath(Arrays.asList(certs)).getEncoded(encoding);
		}
		catch (CertificateException ex) {
			throw new CryptoException(m_res.getString(errkey), ex);
		}
	}

	/**
	 * Generate a self-signed X509 Version 1 certificate for the supplied key pair and signature algorithm.
	 * 
	 * @return The generated certificate
	 * @param sCommonName Common name certficate attribute
	 * @param sOrganisationUnit Organisation Unit certificate attribute
	 * @param sOrganisation Organisation certificate attribute
	 * @param sLocality Locality certificate
	 * @param sState State certificate attribute
	 * @param sEmailAddress Email Address certificate attribute
	 * @param sCountryCode Country Code certificate attribute
	 * @param notBefore
	 * @param notAfter
	 * @param publicKey Public part of key pair
	 * @param privateKey Private part of key pair
	 * @param signatureAlgorithm the signature Algorithm
	 * @param providerName
	 * @throws CryptoException If there was a problem generating the certificate
	 */
	public static X509Certificate generateCert(String sCommonName, String sOrganisationUnit, String sOrganisation,
			String sLocality, String sState, String sCountryCode, String sEmailAddress, Date notBefore, Date notAfter,
			PublicKey publicKey, PrivateKey privateKey, String signatureAlgorithm, String providerName)
			throws CryptoException {
		// Holds certificate attributes
		Hashtable attrs = new Hashtable();
		Vector vOrder = new Vector();

		// Load certificate attributes
		if (!isEmpty(sCommonName)) {
			attrs.put(X509Principal.CN, sCommonName);
			vOrder.add(0, X509Principal.CN);
		}

		if (!isEmpty(sOrganisationUnit)) {
			attrs.put(X509Principal.OU, sOrganisationUnit);
			vOrder.add(0, X509Principal.OU);
		}

		if (!isEmpty(sOrganisation)) {
			attrs.put(X509Principal.O, sOrganisation);
			vOrder.add(0, X509Principal.O);
		}

		if (!isEmpty(sLocality)) {
			attrs.put(X509Principal.L, sLocality);
			vOrder.add(0, X509Principal.L);
		}

		if (!isEmpty(sState)) {
			attrs.put(X509Principal.ST, sState);
			vOrder.add(0, X509Principal.ST);
		}

		if (!isEmpty(sCountryCode)) {
			attrs.put(X509Principal.C, sCountryCode);
			vOrder.add(0, X509Principal.C);
		}

		if (!isEmpty(sEmailAddress)) {
			attrs.put(X509Principal.E, sEmailAddress);
			vOrder.add(0, X509Principal.E);
		}

		// Get an X509 Version 3 Certificate generator
		X509V3CertificateGenerator certGen = new X509V3CertificateGenerator();

		// Load the generator with generation parameters

		// Set the issuer distinguished name
		certGen.setIssuerDN(new X509Principal(vOrder, attrs));

		// Valid before and after dates now to iValidity days in the future
		certGen.setNotBefore(notBefore);
		certGen.setNotAfter(notAfter);

		// Set the subject distinguished name (same as issuer for our purposes)
		certGen.setSubjectDN(new X509Principal(vOrder, attrs));

		// Set the public key
		certGen.setPublicKey(publicKey);

		// Set the algorithm
		certGen.setSignatureAlgorithm(signatureAlgorithm);

		// Set the serial number
		certGen.setSerialNumber(generateX509SerialNumber());

		try {
			// Generate an X.509 certificate, based on the current issuer and
			// subject
			X509Certificate cert = certGen.generate(privateKey, providerName);

			// Return the certificate
			return cert;
		}
		// Something went wrong
		catch (GeneralSecurityException ex) {
			throw new CryptoException(m_res.getString("CertificateGenFailed.exception.message"), ex);
		}
	}

	/**
	 * Generate a self-signed X509 Version 1 certificate for the supplied key pair and signature algorithm by using
	 * <b>BC</b> provider.
	 * 
	 * @return The generated certificate
	 * @param sCommonName Common name certficate attribute
	 * @param sOrganisationUnit Organisation Unit certificate attribute
	 * @param sOrganisation Organisation certificate attribute
	 * @param sLocality Locality certificate
	 * @param sState State certificate attribute
	 * @param sEmailAddress Email Address certificate attribute
	 * @param sCountryCode Country Code certificate attribute
	 * @param notBefore
	 * @param notAfter
	 * @param publicKey Public part of key pair
	 * @param privateKey Private part of key pair
	 * @param signatureAlgorithm the signature Algorithm
	 * @throws CryptoException If there was a problem generating the certificate
	 */
	public static X509Certificate generateCert(String sCommonName, String sOrganisationUnit, String sOrganisation,
			String sLocality, String sState, String sCountryCode, String sEmailAddress, Date notBefore, Date notAfter,
			PublicKey publicKey, PrivateKey privateKey, String signatureAlgorithm) throws CryptoException {
		return generateCert(sCommonName, sOrganisationUnit, sOrganisation, sLocality, sState, sCountryCode,
				sEmailAddress, notBefore, notAfter, publicKey, privateKey, signatureAlgorithm, "BC");
	}

	static boolean isEmpty(String s) {
		return StringUtil.isEmpty(s);
	}

	/**
	 * Generate a unique serial number for use as an X509 serial number.
	 * 
	 * @return The unique serial number
	 */
	private static BigInteger generateX509SerialNumber() {
		// Time in seconds
		return new BigInteger(Long.toString(System.currentTimeMillis() / 1000));
	}

	/**
	 * Create a PKCS #10 certification request (CSR) using the supplied certificate and private key.
	 * 
	 * @param cert The certificate
	 * @param privateKey The private key
	 * @throws CryptoException If there was a problem generating the CSR
	 * @return The CSR
	 */
	public static PKCS10CertificationRequest generatePKCS10CSR(X509Certificate cert, PrivateKey privateKey,
			String providerName) throws CryptoException {
		try {
			// fix bug: if there are some specifical char like <code>", \</code>, the subject in generated csr does not
			// extremely equals to the original one
			// see http://192.168.8.73:8090/browse/SDK-48
			PKCS10CertificationRequest csr = new PKCS10CertificationRequest(cert.getSigAlgName(), cert
					.getSubjectX500Principal(), cert.getPublicKey(), null, privateKey, providerName);
			if (!csr.verify()) {
				throw new CryptoException(m_res.getString("NoVerifyGenCsr.exception.message"));
			}

			return csr;
		}
		catch (GeneralSecurityException ex) {
			throw new CryptoException(m_res.getString("NoGenerateCsr.exception.message"), ex);
		}
	}

	/**
	 * Create a PKCS #10 certification request (CSR) using the supplied certificate and private key.
	 * 
	 * @param cert The certificate
	 * @param privateKey The private key
	 * @throws CryptoException If there was a problem generating the CSR
	 * @return The CSR
	 */
	public static PKCS10CertificationRequest generatePKCS10CSR(X509Certificate cert, PrivateKey privateKey)
			throws CryptoException {
		return generatePKCS10CSR(cert, privateKey, "BC");
	}

	/**
	 * Create a PKCS #10 certification request (CSR) using the supplied certificate and private key. <b>The exist file
	 * will be overwirtten anyway.</b>
	 * 
	 * @param cert The certificate
	 * @param privateKey The private key
	 * @param outputCsr the generated csr destination file
	 * @throws CryptoException If there was a problem generating the CSR
	 * @throws IOException
	 */
	public static void generatePKCS10CSR(X509Certificate cert, PrivateKey privateKey, File outputCsr,
			String providerName) throws CryptoException, IOException {
		PKCS10CertificationRequest req = generatePKCS10CSR(cert, privateKey, providerName);
		PEMWriter writer = new PEMWriter(new FileWriter(outputCsr));
		writer.writeObject(req);
		writer.close();
	}

	/**
	 * Create a PKCS #10 certification request (CSR) using the supplied certificate and private key. <b>The exist file
	 * will be overwirtten anyway.</b>
	 * 
	 * @param cert The certificate
	 * @param privateKey The private key
	 * @param outputCsr the generated csr destination file
	 * @throws CryptoException If there was a problem generating the CSR
	 * @throws IOException
	 */
	public static void generatePKCS10CSR(X509Certificate cert, PrivateKey privateKey, File outputCsr)
			throws CryptoException, IOException {
		PKCS10CertificationRequest req = generatePKCS10CSR(cert, privateKey);
		PEMWriter writer = new PEMWriter(new FileWriter(outputCsr));
		writer.writeObject(req);
		writer.close();
	}

	/**
	 * Verify that one X.509 certificate was signed using the private key that corresponds to the public key of a second
	 * certificate.
	 * 
	 * @return True if the first certificate was signed by private key corresponding to the second signature
	 * @param signedCert The signed certificate
	 * @param signingCert The signing certificate
	 * @throws CryptoException If there was a problem verifying the signature.
	 */
	public static boolean verifyCertificate(X509Certificate signedCert, X509Certificate signingCert)
			throws CryptoException {
		try {
			signedCert.verify(signingCert.getPublicKey());
		}
		// Verification failed
		catch (InvalidKeyException ex) {
			return false;
		}
		// Verification failed
		catch (SignatureException ex) {
			return false;
		}
		// Problem verifying
		catch (GeneralSecurityException ex) {
			throw new CryptoException(m_res.getString("NoVerifyCertificate.exception.message"), ex);
		}
		return true;
	}

	/**
	 * Check whether or not a trust path exists between the supplied X.509 certificate and and the supplied keystores
	 * based on the trusted certificates contained therein, ie that a chain of trust exists between the supplied
	 * certificate and a self-signed trusted certificate in the keystores.
	 * 
	 * @return The trust chain, or null if trust could not be established
	 * @param cert The certificate
	 * @param keyStores The keystores
	 * @throws CryptoException If there is a problem establishing trust
	 */
	public static X509Certificate[] establishTrust(KeyStore[] keyStores, X509Certificate cert) throws CryptoException {
		// Extract all certificates from the Keystores creating
		ArrayList ksCerts = new ArrayList();
		for (int iCnt = 0; iCnt < keyStores.length; iCnt++) {
			ksCerts.addAll(extractCertificates(keyStores[iCnt]));
		}

		// Try and establish trust against the set of all certificates
		return establishTrust(ksCerts, cert);
	}

	/**
	 * Check whether or not a trust path exists between the supplied X.509 certificate and and the supplied comparison
	 * certificates based on the trusted certificates contained therein, ie that a chain of trust exists between the
	 * supplied certificate and a self-signed trusted certificate in the comparison set.
	 * 
	 * @return The trust chain, or null if trust could not be established
	 * @param cert The certificate
	 * @param vCompCerts The comparison set of certificates
	 * @throws CryptoException If there is a problem establishing trust
	 */
	private static X509Certificate[] establishTrust(List vCompCerts, X509Certificate cert) throws CryptoException {
		// For each comparison certificate...
		for (int iCnt = 0; iCnt < vCompCerts.size(); iCnt++) {
			X509Certificate compCert = (X509Certificate) vCompCerts.get(iCnt);

			// Check if the Comparison certificate's subject is the same as the
			// certificate's issuer
			if (cert.getIssuerDN().equals(compCert.getSubjectDN())) {
				// If so verify with the comparison certificate's corresponding
				// private key was used to sign the certificate
				if (X509CertUtil.verifyCertificate(cert, compCert)) {
					// If the keystore certificate is self-signed then a
					// chain of trust exists
					if (compCert.getSubjectDN().equals(compCert.getIssuerDN())) {
						return new X509Certificate[] {
								cert,
								compCert
						};
					}
					// Otherwise try and establish a chain of trust for
					// the comparison certificate against the other comparison
					// certificates
					X509Certificate[] tmpChain = establishTrust(vCompCerts, compCert);
					if (tmpChain != null) {
						X509Certificate[] trustChain = new X509Certificate[tmpChain.length + 1];
						trustChain[0] = cert;
						for (int j = 1; j <= tmpChain.length; j++) {
							trustChain[j] = tmpChain[j - 1];
						}
						return trustChain;
					}
				}
			}
		}

		// No chain of trust
		return null;
	}

	/**
	 * Extract a copy of all trusted certificates contained within the supplied keystore.
	 * 
	 * @param keyStore The keystore
	 * @return The extracted certificates
	 * @throws CryptoException If a problem is encountered extracting the certificates
	 */
	private static Collection extractCertificates(KeyStore keyStore) throws CryptoException {
		try {
			ArrayList vCerts = new ArrayList();

			for (Enumeration en = keyStore.aliases(); en.hasMoreElements();) {
				String sAlias = (String) en.nextElement();

				if (keyStore.isCertificateEntry(sAlias)) {
					vCerts.add(X509CertUtil.convertCertificate(keyStore.getCertificate(sAlias)));
				}
			}

			return vCerts;
		}
		catch (KeyStoreException ex) {
			throw new CryptoException(m_res.getString("NoExtractCertificates.exception.message"), ex);
		}
	}

	/**
	 * Check whether or not a trusted certificate in the supplied keystore matches the the supplied X.509 certificate.
	 * 
	 * @return The alias of the matching certificate in the keystore or null if there is no match
	 * @param cert The certificate
	 * @param keyStore The keystore
	 * @throws CryptoException If there is a problem establishing trust
	 */
	public static String matchCertificate(KeyStore keyStore, X509Certificate cert) throws CryptoException {
		try {
			for (Enumeration en = keyStore.aliases(); en.hasMoreElements();) {
				String sAlias = (String) en.nextElement();
				if (keyStore.isCertificateEntry(sAlias)) {
					X509Certificate compCert = X509CertUtil.convertCertificate(keyStore.getCertificate(sAlias));

					if (cert.equals(compCert)) {
						return sAlias;
					}
				}
			}
			return null;
		}
		catch (KeyStoreException ex) {
			throw new CryptoException(m_res.getString("NoMatchCertificate.exception.message"), ex);
		}
	}

	/**
	 * For a given X.509 certificate get a representative alias for it in a keystore. For a self-signed certificate this
	 * will be the subject's common name (if any). For a non-self-signed certificate it will be the subject's common
	 * name followed by the issuer's common name in parenthesis.
	 * 
	 * @param cert The certificate
	 * @return The alias or a blank string if none could be worked out
	 */
	public static String getCertificateAlias(X509Certificate cert) {
		X500Principal subject = cert.getSubjectX500Principal();
		X500Principal issuer = cert.getIssuerX500Principal();

		String sSubjectCN = NameUtil.getCommonName(subject);

		// Could not get a subject CN - return blank
		if (sSubjectCN == null) {
			return "";
		}

		String sIssuerCN = NameUtil.getCommonName(issuer);

		// Self-signed certificate or could not get an issuer CN
		if (subject.equals(issuer) || sIssuerCN == null) {
			// Alias is the subject CN
			return sSubjectCN;
		}
		// else non-self-signed certificate
		// Alias is the subject CN followed by the issuer CN in parenthesis
		return MessageFormat.format("{0} ({1})", sSubjectCN, sIssuerCN);
	}

	/**
	 * 
	 * @param cert
	 * @return
	 * @throws IOException
	 */
	public static String loadCertificateInPEMEncoding(X509Certificate cert) throws IOException {
		StringWriter sw = new StringWriter();
		PEMWriter pw = new PEMWriter(sw);
		pw.writeObject(cert);
		pw.close();
		return sw.getBuffer().toString();
	}

	/**
	 * Attempt to order the supplied list of X.509 certificates in <b>issued to</b> to <b>issued from</b> order.
	 * 
	 * @param certs
	 * @return the sotred certificate chain in <b>issued to</b> to <b>issued from</b> order
	 * @throws CryptoException if the certificate chain is borken
	 */
	public static List<X509Certificate> sortCertificates(List<X509Certificate> certs) throws CryptoException {
		if (certs.size() < 2) {
			return certs;
		}

		// find end-entity cert
		List<X509Certificate> retList = new ArrayList<X509Certificate>(certs.size());
		List<X509Certificate> originalList = new ArrayList<X509Certificate>(certs);

		X509Certificate rootCert = findEndEntityCertificate(originalList);

		retList.add(rootCert);
		originalList.remove(rootCert);

		X509Certificate currentIssuer = rootCert;
		while (true) {
			if (originalList.size() == 0)
				break;

			X509Certificate issuerTo = findIssuerToCertificate(currentIssuer, originalList);
			if (issuerTo == null) {
				if (originalList.size() > 0) {
					throw new CryptoException("Broken certificate chain");
				}
			}
			else {
				retList.add(issuerTo);
				originalList.remove(issuerTo);
				currentIssuer = issuerTo;
			}
		}

		// reverse to issued to to issued from order.
		Collections.reverse(retList);
		return retList;
	}

	/**
	 * 
	 * @param certs
	 * @return the end or root certificate
	 * @throws CryptoException if the certificate chain is borken
	 */
	public static X509Certificate findEndEntityCertificate(List<X509Certificate> certs) throws CryptoException {
		if (certs.size() == 1) {
			return certs.get(0);
		}

		ArrayList<X509Certificate> rootList = new ArrayList<X509Certificate>();
		for (int i = 0; i < certs.size(); i++) {
			X509Certificate cert = certs.get(i);
			if (cert.getIssuerDN().equals(cert.getSubjectDN())) {
				rootList.add(cert);
				continue;
			}

			X509Certificate issuerCert = findIssuerCertificate(cert, certs);
			if (issuerCert == null) {
				rootList.add(cert);
				continue;
			}
		}
		// can only have one end entity cert - something's wrong, give up.
		if (rootList.size() > 1) {
			StringBuffer msg = new StringBuffer();
			msg.append("more than one root certificate found: \n");
			for (X509Certificate cert : rootList) {
				msg.append(cert.getSubjectDN().getName());
				msg.append("\n");
			}
			throw new CryptoException(msg.toString());
		}

		return rootList.get(0);
	}

	/**
	 * find the direct issuer
	 * 
	 * @param subject
	 * @param certs
	 * @return the direct issuer, or <code>null</code> if not found
	 */
	public static X509Certificate findIssuerCertificate(X509Certificate subject, List<X509Certificate> certs) {
		for (X509Certificate cert : certs) {
			if (!cert.equals(subject)) {
				if (equalsPrincipal(subject.getIssuerDN(), cert.getSubjectDN())) {
					return cert;
				}
			}
		}
		return null;
	}

	public static boolean equalsPrincipal(Principal issuer, Principal subject) {
		// equals via x509name first
		X509Name name1 = new X509Name(issuer.toString());
		X509Name name2 = new X509Name(subject.toString());
		if (name1.equals(name2))
			return true;

		// to avoid side effect, remain the old code below
		String issuerName = issuer.getName();
		String[] segs = issuerName.split(",");
		for (int i = 0; i < segs.length; i++) {
			segs[i] = trimAttr(segs[i].trim());
		}
		Arrays.sort(segs);

		String subjectName = subject.getName();
		String[] segs2 = subjectName.split(",");
		for (int i = 0; i < segs2.length; i++) {
			segs2[i] = trimAttr(segs2[i].trim());
		}
		Arrays.sort(segs2);

		return Arrays.equals(segs, segs2);
	}

	/**
	 * workaroud for bug: <code>O="USA\n"</code>
	 * 
	 * @param str
	 * @return
	 */
	private static String trimAttr(String str) {
		int idx = str.indexOf('=');
		if (idx > 0) {
			String val = str.substring(idx + 1);
			if (val.startsWith("\""))
				val = val.substring(1);
			if (val.endsWith("\""))
				val = val.substring(0, val.length() - 1);
			val = val.trim();
			return str.substring(0, idx + 1) + val;
		}
		return str;
	}

	/**
	 * 
	 * @param issuer
	 * @param certs
	 * @return
	 */
	public static X509Certificate findIssuerToCertificate(X509Certificate issuer, List<X509Certificate> certs) {
		for (X509Certificate cert : certs) {
			if (!cert.equals(issuer)) {
				if (equalsPrincipal(cert.getIssuerDN(), issuer.getSubjectDN())) {
					return cert;
				}
			}
		}
		return null;
	}
}
